feat(agent_api_keys): one-call webhook-trigger setup endpoint#329
feat(agent_api_keys): one-call webhook-trigger setup endpoint#329danielmillerp wants to merge 3 commits into
Conversation
Add POST /agent_api_keys/webhook-trigger: registers a github/slack signature key for an agent and returns the ready-to-paste forward webhook URL + secret in one call. Bundles the existing key-create with webhook-URL composition so a UI (or a curl) can wire a trigger in a single step; the webhook then flows through the existing /agents/forward ingress that verifies the signature against this key. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fdadd80 to
6ca6f70
Compare
✱ Stainless preview buildsThis PR will update the openapi python typescript Edit this comment to update them. They will appear in their respective SDK's changelogs. ✅ agentex-sdk-typescript studio · code · diff
✅ agentex-sdk-openapi studio · code · diff
✅ agentex-sdk-python studio · code · diff
This comment is auto-generated by GitHub Actions and is automatically kept up to date as you push. |
| the signature against this key. Bundles the existing key-create + URL composition so | ||
| a UI (or a curl) can set up a trigger without two steps. | ||
| """ | ||
| if request.source not in (AgentAPIKeyType.GITHUB, AgentAPIKeyType.SLACK): |
There was a problem hiding this comment.
Are there other webhooks we want to scope out (linear, notion, pager duty, datadog?) Is there a way to make it easier to add a key than updating the code?
There was a problem hiding this comment.
Deferring to follow-ups, keeping this PR scoped to github/slack. Each new source (Linear, Notion, PagerDuty, Datadog) needs its own signature-verification scheme wired into the forward path — Linear is HMAC-SHA256 like GitHub, but PagerDuty/Datadog/Notion differ — so it's real per-source work rather than just an enum value. When we add the next source we'll introduce a small {source: signature_scheme} registry so adding one becomes a single entry instead of scattered changes. Tracking that as the extension point.
|
Will we want a rotate path as well should we want to rotate the api key? |
Good call — deferring a dedicated rotate path to a follow-up, keeping this PR to the create flow. Today rotation works by delete + recreate: the forward URL/path is derived from agent name + forward_path (not the secret), so deleting the key and re-creating the trigger yields a new secret to paste while the webhook URL stays the same. A dedicated |
| ), | ||
| ) | ||
|
|
||
| secret = request.secret or secrets.token_hex(32) |
There was a problem hiding this comment.
Empty string for
secret is falsy in Python, so request.secret or secrets.token_hex(32) will generate a new random secret rather than treating the empty string as-is. A caller who sends "secret": "" intending to set up a no-secret GitHub webhook (GitHub allows omitting the secret, in which case it sends no X-Hub-Signature-256 header) would receive a generated secret, register it on the key, but configure GitHub with no secret — causing every delivery to be rejected with an invalid-signature failure. Replacing or with an explicit None check avoids the silent coercion.
| secret = request.secret or secrets.token_hex(32) | |
| secret = request.secret if request.secret is not None else secrets.token_hex(32) |
Prompt To Fix With AI
This is a comment left during a code review.
Path: agentex/src/api/routes/agent_api_keys.py
Line: 157
Comment:
Empty string for `secret` is falsy in Python, so `request.secret or secrets.token_hex(32)` will generate a new random secret rather than treating the empty string as-is. A caller who sends `"secret": ""` intending to set up a no-secret GitHub webhook (GitHub allows omitting the secret, in which case it sends no `X-Hub-Signature-256` header) would receive a generated secret, register it on the key, but configure GitHub with no secret — causing every delivery to be rejected with an invalid-signature failure. Replacing `or` with an explicit `None` check avoids the silent coercion.
```suggestion
secret = request.secret if request.secret is not None else secrets.token_hex(32)
```
How can I resolve this? If you propose a fix, please make it concise.Slack signs requests with the app's existing Signing Secret, not a per-webhook secret we can generate. Auto-generating one for a Slack trigger stored a random value that would never match, so every real Slack delivery failed signature verification. Now Slack requires the caller to supply 'secret'; GitHub still auto-generates. Addresses Greptile P1. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
f3a13f8 to
949cc2d
Compare
| forward_path = request.forward_path.lstrip("/") | ||
| webhook_path = f"/agents/forward/name/{request.agent_name}/{forward_path}" |
There was a problem hiding this comment.
forward_path is inserted into the returned URL as raw text, so URL delimiters can change what the forward route receives. For example, forward_path="github-pr/cfg-9?mode=review" returns /agents/forward/name/<agent>/github-pr/cfg-9?mode=review; when GitHub posts to it, FastAPI receives github-pr/cfg-9 as the path and mode=review as the query string, so the agent never gets the configured subpath. #, spaces, and other reserved characters have similar effects. Encode the path segment or reject query, fragment, and control characters before returning the pasteable URL.
Artifacts
Repro: focused FastAPI pytest for raw forward_path delimiter routing
- Contains supporting evidence from the run (text/x-python; charset=utf-8).
- Keeps the command output available without making the summary code-heavy.
Ran code and verified through T-Rex
Prompt To Fix With AI
This is a comment left during a code review.
Path: agentex/src/api/routes/agent_api_keys.py
Line: 166-167
Comment:
**Raw path breaks routing**
`forward_path` is inserted into the returned URL as raw text, so URL delimiters can change what the forward route receives. For example, `forward_path="github-pr/cfg-9?mode=review"` returns `/agents/forward/name/<agent>/github-pr/cfg-9?mode=review`; when GitHub posts to it, FastAPI receives `github-pr/cfg-9` as the path and `mode=review` as the query string, so the agent never gets the configured subpath. `#`, spaces, and other reserved characters have similar effects. Encode the path segment or reject query, fragment, and control characters before returning the pasteable URL.
How can I resolve this? If you propose a fix, please make it concise.| secret: str | None = Field( | ||
| None, | ||
| description="Optional signing secret; if unset, one is generated and returned.", | ||
| ) |
There was a problem hiding this comment.
The route now returns 400 when source is slack and secret is omitted, but this request schema still documents secret as optional and generated when unset. Generated clients and UI built from this schema can omit the Slack signing secret, then every Slack setup call fails instead of completing the one-call trigger flow.
Artifacts
Repro: FastAPI TestClient schema and endpoint harness
- Contains supporting evidence from the run (text/x-python; charset=utf-8).
Repro: OpenAPI schema excerpt and Slack endpoint 400 response output
- Keeps the command output available without making the summary code-heavy.
Ran code and verified through T-Rex
Prompt To Fix With AI
This is a comment left during a code review.
Path: agentex/src/api/schemas/agent_api_keys.py
Line: 77-80
Comment:
**Slack contract is stale**
The route now returns `400` when `source` is `slack` and `secret` is omitted, but this request schema still documents `secret` as optional and generated when unset. Generated clients and UI built from this schema can omit the Slack signing secret, then every Slack setup call fails instead of completing the one-call trigger flow.
How can I resolve this? If you propose a fix, please make it concise.
What
Adds
POST /agent_api_keys/webhook-trigger— wires a webhook trigger in one call:github/slacksignature-verification key for the agent (auto-generates the signing secret if not provided),Why
The pieces to trigger an agent from a webhook already exist (the
/agents/forwardingress verifies the signature against an agent key, andPOST /agent_api_keysregisters keys). This bundles key-create + webhook-URL composition so a UI (or a curl) can set up a trigger in a single step instead of two — the backend for the self-serve "Add trigger" button. The webhook then flows through the existing forward ingress unchanged.Before — wiring a trigger was two manual steps, and the caller had to know the agent's internal id, invent a secret, and hand-compose the forward URL:
After — one call, by agent name, returns the URL + secret ready to paste:
No new ingress, no migration — built on the existing
agent_api_keys+ forward mechanism.On the signing secret
This matches how GitHub webhooks actually work. A GitHub webhook's Secret is a value you supply (GitHub doesn't generate one) — you type a high-entropy string into the webhook's Secret field, and GitHub uses it to HMAC each payload into
X-Hub-Signature-256: sha256=…, which the receiver re-computes and compares. It's a shared secret that must exist on both sides (GitHub's config + the verifying server), and it's optional-but-recommended (no secret → no signature header at all). Refs: GitHub docs validating-webhook-deliveries, creating-webhooks.Given that, the endpoint's
secrethandling is deliberate:secret(default) → the endpoint generates a strong one (secrets.token_hex(32)), stores it on the agent's verification key, and returns it once to paste into GitHub's Secret field — so the caller never has to invent entropy.secret→ for when the GitHub webhook already has a secret set and the agent side just needs to match it.Testing
🤖 Generated with Claude Code
Greptile Summary
POST /agent_api_keys/webhook-triggerto create webhook verification keys by agent name.Confidence Score: 4/5
The endpoint is close but should not merge until URL construction and the Slack request contract are corrected.
Focused endpoint and schema coverage are present, and runtime checks confirm the two main compatibility issues in the new setup flow.
agentex/src/api/routes/agent_api_keys.py and agentex/src/api/schemas/agent_api_keys.py
What T-Rex did
Prompt To Fix All With AI
Reviews (5): Last reviewed commit: "fix(agent_api_keys): require provided se..." | Re-trigger Greptile